﻿using System.Web;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.Logging;
using Microsoft.Net.Http.Headers;

namespace Devonline.Identity;

/// <summary>
/// identity HttpClient service
/// </summary>
public class IdentityService
{
    private readonly ILogger<IdentityService> _logger;
    private readonly HttpSetting _httpSetting;
    private readonly HttpContext _httpContext;
    private readonly IdentityDbContext _dbContext;
    private readonly AuthorizationService _authorizationService;
    private readonly PhoneNumberCaptchaService _phoneNumberCaptchaService;
    private readonly SignInManager<User> _signInManager;
    private readonly ICaptchaService _captchaService;
    private readonly IHttpClientFactory _httpClientFactory;
    private readonly IDistributedCache _cache;
    public IdentityService(
        ILogger<IdentityService> logger,
        HttpSetting httpSetting,
        IdentityDbContext dbContext,
        AuthorizationService authorizationService,
        PhoneNumberCaptchaService phoneNumberCaptchaService,
        SignInManager<User> signInManager,
        ICaptchaService captchaService,
        IHttpContextAccessor httpContextAccessor,
        IHttpClientFactory httpClientFactory,
        IDistributedCache cache
        )
    {
        ArgumentNullException.ThrowIfNull(httpContextAccessor.HttpContext);
        ArgumentNullException.ThrowIfNull(httpSetting.Identity);
        _logger = logger;
        _httpSetting = httpSetting;
        _httpContext = httpContextAccessor.HttpContext;
        _dbContext = dbContext;
        _cache = cache;
        _authorizationService = authorizationService;
        _phoneNumberCaptchaService = phoneNumberCaptchaService;
        _signInManager = signInManager;
        _captchaService = captchaService;
        _httpClientFactory = httpClientFactory;
    }

    private const string ErrorMessageInvalidCredentials = "用户名或密码不正确, 请重新输入!";
    private const string ErrorMessageLockedOut = "当前用户已被锁定, 请联系管理员处理!";
    private const string ErrorMessageNotAllowed = "当前用户不允许登陆, 请联系管理员处理!";
    private const string ErrorMessagePhoneNumberUserNotFound = "当前手机号码的用户不存在!";
    private const string ErrorMessageSmsCaptchaExpired = "短信验证码已过期!";
    private const string ErrorMessageSmsCaptchaError = "短信验证码错误, 请重新输入!";

    /// <summary>
    /// login to Identity Service
    /// </summary>
    /// <param name="loginModel"></param>
    /// <returns></returns>
    public async Task<LoginResultViewModel> LoginAsync(UserLoginModel loginModel)
    {
        ArgumentNullException.ThrowIfNull(_httpSetting.UserInteraction);
        _logger.LogDebug("user {user} will login to Identity Service!", loginModel.UserName);
        //var httpContent = new StringContent(model.ToJsonString());
        //httpContent.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue(ContentType.Json);
        //using var httpClient = _httpClientFactory.CreateClient(nameof(IdentityService));
        //using var response = await httpClient.PostAsync(_httpSetting.UserInteraction.Login, httpContent);
        //if (!response.IsSuccessStatusCode)
        //{
        //    var content = await response.Content.ReadAsStringAsync();
        //    throw new BadHttpRequestException("登录失败!", (int)response.StatusCode, new Exception("登录失败, 详情: " + content));
        //}

        var loginResult = new LoginResultViewModel { Type = loginModel.Type, UserName = loginModel.UserName!, Success = false, RedirectUri = loginModel.ReturnUrl };
        var (result, errorMessage) = await SignInAsync(loginModel, false);
        if (result)
        {
            loginResult.Success = true;
            _logger.LogInformation($"用户: {loginModel.UserName} 登录成功!");
        }
        else
        {
            loginResult.ErrorMessage = errorMessage;
            _logger.LogInformation($"用户: {loginModel.UserName} 登录失败, 错误提示: {errorMessage}");
        }

        return loginResult;
    }
    /// <summary>
    /// 登录
    /// </summary>
    /// <returns></returns>
    public async Task<UserInfo> TokenLoginAsync(UserLoginModel model)
    {
        ArgumentNullException.ThrowIfNull(model.UserName);
        _logger.LogDebug("user {user} will login from Identity Service!", model.UserName);
        var token = await GetIdentityTokenAsync(model);
        ArgumentNullException.ThrowIfNull(token);
        using var httpClient = _httpClientFactory.CreateClient(nameof(IdentityService));
        httpClient.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue(DEFAULT_AUTHENTICATION_SCHEME, token.AccessToken);
        var identityUserInfo = await GetIdentityUserInfoAsync(model.UserName);
        ArgumentNullException.ThrowIfNull(identityUserInfo);
        _logger.LogInformation("user {user} login success and get the token and userinfo from Identity Service!", model.UserName);
        return new UserInfo { Token = token, User = identityUserInfo.CopyTo<UserViewModel>() };
    }
    /// <summary>
    /// 退出登录
    /// </summary>
    /// <returns></returns>
    public async Task LogoutAsync()
    {
        var userName = _httpContext.GetUserName();
        _logger.LogDebug("user {user} will logout from Identity Service!", userName);
        ArgumentNullException.ThrowIfNull(_httpSetting.UserInteraction);
        using var httpClient = _httpClientFactory.CreateClient(nameof(IdentityService));
        using var response = await httpClient.GetAsync(_httpSetting.UserInteraction.Logout);
        var result = response.IsSuccessStatusCode ? "success" : (await response.Content.ReadAsStringAsync());
        httpClient.DefaultRequestHeaders.Remove("Cookie");
        httpClient.DefaultRequestHeaders.Remove("Authorization");
        _logger.LogInformation("user {user} logout from Identity Service: " + result, userName);
    }

    /// <summary>
    /// 登陆核心方法
    /// </summary>
    /// <param name="loginModel">登录的视图数据对象模型</param>
    /// <param name="isCaptchaValid">是否需要验证码</param>
    /// <returns></returns>
    private async Task<(bool, string)> SignInAsync(UserLoginModel loginModel, bool isCaptchaValid = true)
    {
        _logger.LogDebug("user {user} request to login system", loginModel.UserName);
        if (string.IsNullOrWhiteSpace(loginModel.UserName) || string.IsNullOrWhiteSpace(loginModel.Password))
        {
            _logger.LogDebug("user {user} request to login system but not provid userName and password!", loginModel.UserName);
            return (false, loginModel.Type switch
            {
                LoginType.Password => "必须输入用户名和密码!",
                LoginType.Captcha => "必须输入验证码!",
                _ => "登录方式无效!"
            });
        }

        loginModel.UserName = loginModel.UserName.ToLowerInvariant();
        if (loginModel.UserName == nameof(AuthorizeType.System) || loginModel.UserName == nameof(AuthorizeType.Anonymous))
        {
            _logger.LogDebug("user {user} access can not be allow to login system!", loginModel.UserName);
            return (false, loginModel.Type switch
            {
                LoginType.Password => "当前用户账户不可登录系统!",
                LoginType.Captcha => "当前用户账户不可登录系统!",
                _ => "登录方式无效!"
            });
        }

        if (_httpContext.User.Identity?.IsAuthenticated ?? false)
        {
            _logger.LogDebug("user {user} already login success", loginModel.UserName);
            return (true, string.Empty);
        }

        if (isCaptchaValid && _httpSetting.CaptchaExpireTime > UNIT_ZERO)
        {
            if (string.IsNullOrWhiteSpace(loginModel.CaptchaId))
            {
                _logger.LogDebug("user {user} CAPTCHA id is empty!", loginModel.UserName);
                return (false, string.Empty);
            }

            if (string.IsNullOrWhiteSpace(loginModel.CaptchaCode))
            {
                _logger.LogDebug("user {user} CAPTCHA value is empty!", loginModel.UserName);
                return (false, "验证码不能为空!");
            }

            if (!_captchaService.Validate(loginModel.CaptchaId, loginModel.CaptchaCode))
            {
                _logger.LogDebug("user {user} CAPTCHA value is invalid!", loginModel.UserName);
                return (false, "验证码不正确!");
            }
        }

        var errorMessage = ErrorMessageInvalidCredentials;
        switch (loginModel.Type)
        {
            case LoginType.Password:
                _logger.LogDebug("user {user} request to login system for Password type", loginModel.UserName);
                var result = await _signInManager.PasswordSignInAsync(loginModel.UserName, loginModel.Password, loginModel.RememberLogin, lockoutOnFailure: true);
                if (result.Succeeded)
                {
                    var userContext = await _authorizationService.GetUserContextAsync(loginModel.UserName);
                    if (userContext is not null)
                    {
                        var userModel = await _dbContext.Users.FirstOrDefaultAsync(x => x.UserName == loginModel.UserName);
                        if (userModel is not null)
                        {
                            var claims = await userContext.User.GetUserClaimsAsync(_dbContext, _httpSetting.DataIsolate);
                            loginModel.ReturnUrl = HttpUtility.UrlDecode(loginModel.ReturnUrl);
                            await _signInManager.SignInWithClaimsAsync(userModel, new AuthenticationProperties { IsPersistent = false, RedirectUri = loginModel.ReturnUrl }, claims);
                            _logger.LogDebug("user {user} login success for Password type", loginModel.UserName);
                            return (true, string.Empty);
                        }
                    }
                }

                if (result.IsLockedOut)
                {
                    errorMessage = ErrorMessageLockedOut;
                }

                if (result.IsNotAllowed)
                {
                    errorMessage = ErrorMessageNotAllowed;
                }

                break;
            case LoginType.Captcha:
                //此时, userName 和 password 分别是手机号和验证码
                _logger.LogDebug("user {user} request to login system for Captcha type", loginModel.UserName);
                var captcha = await _phoneNumberCaptchaService.ValidateAsync(loginModel.UserName, loginModel.Password);
                if (captcha is null)
                {
                    errorMessage = ErrorMessageSmsCaptchaExpired;
                }
                else if (!string.IsNullOrWhiteSpace(captcha.ErrorMessage))
                {
                    errorMessage = captcha.ErrorMessage;
                }
                else
                {
                    var user = await _dbContext.Users.FirstOrDefaultAsync(x => x.PhoneNumberConfirmed && x.PhoneNumber == loginModel.UserName);
                    if (user is null)
                    {
                        errorMessage = ErrorMessagePhoneNumberUserNotFound;
                    }
                    else
                    {
                        //验证通过, 登录成功
                        var userContext = await _authorizationService.GetUserContextAsync(user.UserName);
                        if (userContext is not null)
                        {
                            var claims = await userContext.User.GetUserClaimsAsync(_dbContext, _httpSetting.DataIsolate);
                            loginModel.ReturnUrl = HttpUtility.UrlDecode(loginModel.ReturnUrl);
                            await _signInManager.SignInWithClaimsAsync(user, new AuthenticationProperties { IsPersistent = false, RedirectUri = loginModel.ReturnUrl }, claims);
                            _logger.LogDebug("user {user} login success for Captcha type", loginModel.UserName);
                            return (true, string.Empty);
                        }
                    }

                    errorMessage = ErrorMessageSmsCaptchaError;
                }
                break;
            default:
                break;
        }

        _logger.LogDebug("user {user} login failed, " + errorMessage, loginModel.UserName);
        return (false, errorMessage);
    }
    /// <summary>
    /// 真正的登录,设置cookie
    /// </summary>
    /// <returns></returns>
    private async Task SignInAsync(HttpResponseMessage httpResponse)
    {
        ArgumentNullException.ThrowIfNull(_httpSetting.UserInteraction);

        var system = _httpContext.Request.Host.ToString();
        using var request = GetHttpRequest(System.Net.Http.HttpMethod.Get, _httpSetting.UserInteraction.UserInfo + $"?{nameof(system)}={system}", httpResponse);
        using var httpClient = _httpClientFactory.CreateClient(nameof(IdentityService));
        using var response = await httpClient.SendAsync(request);
        var result = await response.Content.ReadAsStringAsync();
        if (!response.IsSuccessStatusCode)
        {
            throw new BadHttpRequestException("获取用户信息失败!", (int)response.StatusCode, new Exception("获取用户信息失败, 详情: " + result));
        }

        var userInfo = result.ToJsonObject<UserInfo>();
        if (userInfo is not null && userInfo.User is not null)
        {
            await SignInAsync(userInfo.User);
        }
    }
    /// <summary>
    /// 真正的登录,设置cookie
    /// </summary>
    /// <returns></returns>
    private async Task SignInAsync(UserViewModel user)
    {
        _logger.LogInformation($"user {user.UserName} get userinfo from Identity Service success");
        var claims = await user.GetUserClaimsAsync(_dbContext, _httpSetting.DataIsolate);
        //string scheme = CookieAuthenticationDefaults.AuthenticationScheme;
        //var claimsPrincipal = new ClaimsPrincipal(new ClaimsIdentity(claims, scheme));
        //await _httpContext.SignInAsync(
        //    scheme,
        //    claimsPrincipal,
        //    new AuthenticationProperties
        //    {
        //        IsPersistent = true,
        //        Items = { { nameof(scheme), scheme } }
        //    });

        //_httpContext.User = claimsPrincipal;

        await _signInManager.SignInWithClaimsAsync(user.CopyTo<User>(), true, claims);
        _logger.LogInformation($"user {user.UserName} login from Identity Service success! ");
    }

    /// <summary>
    /// 获取 access token
    /// </summary>
    /// <param name="model"></param>
    /// <returns></returns>
    private async Task<IdentityToken?> GetIdentityTokenAsync(UserLoginModel model)
    {
        ArgumentNullException.ThrowIfNull(_httpSetting.Identity);
        ArgumentNullException.ThrowIfNull(_httpSetting.Identity.ClientId);
        ArgumentNullException.ThrowIfNull(_httpSetting.Identity.ClientSecret);
        ArgumentNullException.ThrowIfNull(_httpSetting.Identity.GrantType);
        _logger.LogDebug("user {user} will get token from Identity Service!", model.UserName);
        var dic = new Dictionary<string, string>();
        var column = _httpSetting.Identity.GetColumnName(nameof(_httpSetting.Identity.ClientId));
        if (column is not null)
        {
            dic.Add(column, _httpSetting.Identity.ClientId);
        }

        column = _httpSetting.Identity.GetColumnName(nameof(_httpSetting.Identity.ClientSecret));
        if (column is not null)
        {
            dic.Add(column, _httpSetting.Identity.ClientSecret);
        }

        column = _httpSetting.Identity.GetColumnName(nameof(_httpSetting.Identity.GrantType));
        if (column is not null)
        {
            dic.Add(column, _httpSetting.Identity.GrantType);
        }

        column = model.GetColumnName(nameof(model.UserName));
        if (column is not null && model.UserName is not null)
        {
            dic.Add(column, model.UserName);
        }

        column = model.GetColumnName(nameof(model.Password));
        if (column is not null && model.Password is not null)
        {
            dic.Add(column, model.Password);
        }

        using var httpContent = new FormUrlEncodedContent(dic);
        httpContent.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue(ContentType.FormUrlencoded);
        using var httpClient = _httpClientFactory.CreateClient(nameof(IdentityService));
        using var response = await httpClient.PostAsync(_httpSetting.Identity.Token, httpContent);
        var result = await response.Content.ReadAsStringAsync();
        if (response.IsSuccessStatusCode)
        {
            var token = result.ToJsonObject<IdentityToken>();
            if (token is not null)
            {
                _logger.LogInformation("user {user} get access token success: " + result, model.UserName);
                return token;
            }
        }

        _logger.LogDebug("user {user} get token from Identity Service failed: " + result, model.UserName);
        return default;
    }
    /// <summary>
    /// get identity userinfo from identity service
    /// </summary>
    /// <param name="userName"></param>
    /// <returns></returns>
    private async Task<IdentityUserInfo?> GetIdentityUserInfoAsync(string userName)
    {
        ArgumentNullException.ThrowIfNull(_httpSetting.Identity);
        _logger.LogDebug("user {user} will get userinfo from Identity Service!", userName);

        using var httpClient = _httpClientFactory.CreateClient(nameof(IdentityService));
        using var response = await httpClient.GetAsync(_httpSetting.Identity.UserInfo);
        var result = await response.Content.ReadAsStringAsync();
        if (response.IsSuccessStatusCode)
        {
            var userInfo = result.ToJsonObject<IdentityUserInfo>();
            if (userInfo is not null)
            {
                userInfo.PhoneNumber = userInfo.PhoneNumber.Desensitize();
                _logger.LogInformation("user {user} get userinfo from Identity Service success: " + result, userName);
                return userInfo;
            }
        }

        _logger.LogDebug("user {user} get userinfo from Identity Service failed: " + result, userName);
        return default;
    }

    /// <summary>
    /// 根据上下文创建新的 Http 请求
    /// </summary>
    /// <param name="httpMethod">请求类型</param>
    /// <param name="url">请求地址</param>
    private HttpRequestMessage GetHttpRequest(System.Net.Http.HttpMethod httpMethod, string url, HttpResponseMessage? response = default)
    {
        var userName = _httpContext.GetUserName();
        var request = new HttpRequestMessage(httpMethod, url);
        var cookies = new List<string>();
        if (_httpContext.Request.Cookies.Any())
        {
            cookies.Add(_httpContext.Request.Headers.Cookie.ToString());
        }

        if (response is not null)
        {
            var setCookies = response.GetSetCookies();
            if (setCookies.Count != 0)
            {
                foreach (var cookie in setCookies)
                {
                    cookies.Add(cookie.Name.ToString() + CHAR_EQUAL + cookie.Value.ToString());
                }
            }
        }

        if (cookies.Count != 0)
        {
            var requestCookies = string.Join(CHAR_SEMICOLON, cookies);
            _logger.LogDebug("user {user} will get user info from Identity Service take the request Cookies: " + requestCookies, userName);
            request.Headers.Add("Cookie", requestCookies);
        }

        if (!string.IsNullOrWhiteSpace(_httpContext.Request.Headers.Authorization) && System.Net.Http.Headers.AuthenticationHeaderValue.TryParse(_httpContext.Request.Headers.Authorization, out var authentication))
        {
            _logger.LogDebug("user {user} will get user info from Identity Service take the request Authorization", userName);
            request.Headers.Authorization = authentication;
        }

        return request;
    }
    /// <summary>
    /// 将接口中返回的 Cookies 设置到 Http 响应中
    /// </summary>
    /// <param name="httpResponse"></param>
    /// <param name="setCookies"></param>
    private void SetResponseCookies(HttpResponse httpResponse, ICollection<SetCookieHeaderValue> setCookies)
    {
        if (setCookies is not null && setCookies.Count != 0)
        {
            foreach (var setCookie in setCookies)
            {
                httpResponse.Cookies.Delete(setCookie.Name.ToString());
                httpResponse.Cookies.Append(setCookie.Name.ToString(), setCookie.Value.ToString(), setCookie.GetCookieOptions());
            }
        }
    }
}